import json
from collections import deque
import redis
import pyaudio
import numpy as np
from threading import Thread
import logging


class RedisListener:
	def __init__(self, host, port, db, password):
		self._client = redis.Redis(host=host, port=port, db=db, password=password)
		self._running = True
		self._audio = pyaudio.PyAudio()

		self._sampling_rate = 44100
		self._frequency = 100
		self._duration = 0.1
		self._stream = self._audio.open(format=pyaudio.paFloat32, channels=2, rate=self._sampling_rate, output=True)
		self._left = deque()
		self._right = deque()

		self._merger = Thread(target=self.merger_function)
		self._merger.start()

	def listen(self):
		while self._running is True:
			try:
				response = self._client.brpop(['action', 'command'], 1)
				if response is None:
					continue
				decoded = response[1].decode('utf-8')
				if response[0] == b'command':
					self._do_command(decoded)
					continue
				elif response[0] == b'action':
					self._do_action(decoded)
			except Exception as error:
				logging.exception(error)
				self._running = False

	def _do_command(self, command):
		if command == 'end':
			self._running = False
			self._stream.stop_stream()
			self._stream.close()
			self._audio.terminate()
			return
		logging.error(f'did not understand command {command}')

	def _do_action(self, action_to_do):
		try:
			action_dict = json.loads(action_to_do)
		except Exception as error:
			logging.exception(error)
			return
		side = action_dict.get('side')
		if side not in ['r', 'l']:
			logging.error(f'unknown side {side}')
			return

		begin = self._float_or_error(action_dict, 'begin')
		end = self._float_or_error(action_dict, 'end')
		seconds = self._float_or_error(action_dict, 'seconds')

		if begin is None or end is None or seconds is None:
			return

		steps = int(seconds / self._duration)
		current_volume = begin
		step_volume_increase = float(end - begin) / float(steps)

		for index in range(steps):
			self._add_sound(side, current_volume)
			current_volume += step_volume_increase
		logging.info(f'action: {action_to_do}')

	def _float_or_error(self, action_dict, name):
		value = action_dict.get(name)
		try:
			value = float(value)
		except Exception as error:
			logging.error(f'wrong value for {name}')
			logging.exception(error)
			return None
		return value

	def merger_function(self):
		while self._running is True:
			right = None
			left = None
			if len(self._right) != 0:
				right = self._right.popleft()
			if len(self._left) != 0:
				left = self._left.popleft()

			if right is not None or left is not None:
				if right is not None:
					merged_sample = right
				else:
					merged_sample = left

				side = -1
				for index in range(len(merged_sample)):
					if side == -1:
						if left is None:
							merged_sample[index] = 0.0
						else:
							merged_sample[index] = left[index]
					else:
						if right is None:
							merged_sample[index] = 0.0
						else:
							merged_sample[index] = right[index]
					side *= -1
				merged_sample = merged_sample.tobytes()
				self._stream.write(merged_sample)

	def _add_sound(self, side, volume):
		samples = (volume * np.sin(2 * np.pi * np.arange(self._sampling_rate * self._duration) * self._frequency / self._sampling_rate)).astype(np.float32)
		if side in ['r', 'rl']:
			self._right.append(samples)
		if side in ['l', 'rl']:
			self._left.append(samples)


if __name__ == '__main__':
	listener = RedisListener('localhost', 6379, 0, None)
	listener.listen()
